INTRODUCTION
Generative Deep Learning
This coursework will explore the use of generative adversarial networks (GANs) to generate synthetic images of flowers. The techniques used within this study will follow the workings of Francois Chollet within the Deep Learning With Python textbook.
GANs a class of deep learning models designed for generating new, previously unseen data that is similar to a given training dataset. GANs consist of two main components: a generator network and a discriminator network. The generator network is responsible for creating new data, while the discriminator network is responsible for distinguishing the generated data from the real data. The two networks are trained simultaneously in an adversarial manner, where the generator tries to create data that can fool the discriminator into thinking it's real, and the discriminator tries to correctly identify the generated data as fake. The training process continues until the generator is able to produce data that is indistinguishable from real data to the discriminator.
The Dataset
The dataset being used is the Oxford 102 Category Flower Dataset, available at https://www.robots.ox.ac.uk/~vgg/data/flowers/102/, containing 102 categories of flowers found in the UK. Each class contains between 40-258 samples with an overall sample size of 8189. To use the images within the model, they must be resized and preprocessed into a tensorflow dataset object that is normalized to squish the pixel values between 0 and 1.
Workflow
To successfully implement a GAN model, the tweaking of varius hyperparameters will take place along with regularization techniques in order to find the most successful model with the intent of producing the most "artistic" or "realistic" looking flower generations. This tuning process includes adjustments to the optimizer, learning rate, layer units, and number of iterations for each model.
The ultimate goal of this research is to identify the specific changes to the GAN model that result in the highest quality generated flower images, as determined by human interpretation.
import numpy as np
import matplotlib.pyplot as plt
import PIL
import os, sys, pathlib
from PIL import Image
import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Reshape, UpSampling2D, Conv2D, BatchNormalization, Conv2DTranspose
from tensorflow.keras.layers import LeakyReLU, Dropout, ZeroPadding2D, Flatten, Activation
from tensorflow.keras.optimizers import Adam
#Create global variables
BATCH = 64
IMG_SIZE = (64,64)
LATENT_DIM = 128
basedir = pathlib.Path("gallery")
imgdir = basedir / "plant"
outputdir = basedir / "generated"
#Importing data
batch_s = int(BATCH/2)
#Import the data and resizing to 64x64 image
data = tf.keras.preprocessing.image_dataset_from_directory(imgdir, label_mode = None, image_size = IMG_SIZE, batch_size = batch_s, smart_resize=True).map(lambda x: x /255.0)
#Display some example images
for sample in data:
break
f,ax = plt.subplots(4,4,figsize=(15,15))
ax=ax.flatten()
for i in range(16):
ax[i].imshow(sample[i])
CREATE GENERATOR / DISCRIMINATOR
def create_generator(latent_dim):
generator=Sequential()
generator.add(Dense(4*4*512,input_shape=[latent_dim]))
generator.add(Reshape([4,4,512]))
generator.add(Conv2DTranspose(128, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(256, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(512, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(3, kernel_size=4, strides=2, padding="same", activation='sigmoid'))
return generator
generator = create_generator(LATENT_DIM)
generator.summary()
def create_discriminator(input_shape):
discriminator=Sequential()
discriminator.add(Conv2D(64, kernel_size=4, strides=2, padding="same",input_shape=input_shape))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(128, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(256, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Flatten())
discriminator.add(Dropout(0.2))
discriminator.add(Dense(1,activation='sigmoid'))
return discriminator
input_shape = (64, 64, 3)
discriminator = create_discriminator(input_shape)
discriminator.summary()
GAN CREATION
class GAN(tf.keras.Model):
def __init__(self, discriminator, generator, latent_dim):
# Initialize the GAN model by calling the super class's constructor
super(GAN, self).__init__()
self.discriminator = discriminator
self.generator = generator
self.latent_dim = latent_dim
def compile(self, d_optimizer, g_optimizer, loss_fn):
# Compile the GAN model by calling the super class's compile method
super(GAN, self).compile()
self.d_optimizer = d_optimizer
self.g_optimizer = g_optimizer
self.loss_fn = loss_fn
# Initialize metrics to track the losses of the discriminator and generator
self.dloss = tf.keras.metrics.Mean(name="discriminator_loss")
self.gloss = tf.keras.metrics.Mean(name="generator_loss")
@property
def metrics(self):
# Return the list of metrics
return [self.dloss, self.gloss]
def train_step(self, real_images):
# Get the batch size and generate noise with the same shape
batch_size = tf.shape(real_images)[0]
noise = tf.random.normal(shape=(batch_size, self.latent_dim))
# Generate fake images
generated_images = self.generator(noise)
# Concatenate the fake and real images
combined_images = tf.concat([generated_images, real_images], axis=0)
# Create labels
labels = tf.concat([tf.ones((batch_size, 1)), tf.zeros((batch_size, 1))], axis=0)
# Add some random noise to the labels to make the training more robust
labels += 0.05 * tf.random.uniform(tf.shape(labels))
# Use the discriminator model to predict the probability of the images being real
with tf.GradientTape() as tape:
predictions = self.discriminator(combined_images)
# Calculate the loss for the discriminator
dloss = self.loss_fn(labels, predictions)
# Calculate the gradients for the discriminator
grads = tape.gradient(dloss, self.discriminator.trainable_weights)
# Update the weights of the discriminator using the optimizer
self.d_optimizer.apply_gradients(zip(grads, self.discriminator.trainable_weights))
# Generate noise with the same shape as the batch
noise = tf.random.normal(shape=(2*batch_size, self.latent_dim))
# Create labels of all zeros
labels = tf.zeros((2*batch_size, 1))
# Use the discriminator model to predict the probability of the fake images being real
with tf.GradientTape() as tape:
predictions = self.discriminator(self.generator(noise))
gloss = self.loss_fn(labels, predictions)
# Calculate the gradients for the generator
grads = tape.gradient(gloss, self.generator.trainable_weights)
# Update the weights of the generator using the optimizer
self.g_optimizer.apply_gradients(zip(grads, self.generator.trainable_weights))
# Update the loss metrics with the new losses
self.dloss.update_state(dloss)
self.gloss.update_state(gloss)
# Return a dictionary with the losses of the discriminator and generator
return {"d_loss": self.dloss.result(), "g_loss": self.gloss.result()}
TRAIN
def train(model, epoch_number):
#Create callback to monitor progress of model by showing generated images
class GANMonitor(tf.keras.callbacks.Callback):
def __init__(self, num_img=3, latent_dim=LATENT_DIM):
self.num_img = num_img
self.latent_dim = latent_dim
def on_epoch_end(self, epoch, logs=None):
random_latent_vectors = tf.random.normal(shape=(self.num_img, self.latent_dim))
generated_images = self.model.generator(random_latent_vectors)
generated_images *= 255
generated_images.numpy()
for i in range(self.num_img):
img = tf.keras.utils.array_to_img(generated_images[i])
img.save(outputdir / f"generated_img_{epoch:03d}_{i}.png")
#Code Partly Provided From Notebook 12.5
#Fit model
return model.fit(data,
epochs = epoch_number,
batch_size = 512,
callbacks=GANMonitor(num_img=10, latent_dim=LATENT_DIM))
PLOT LOSS
def plot_loss():
history_dict = history.history
d_loss = history_dict['d_loss']
g_loss = history_dict['g_loss']
epochs = range(1, len(d_loss) + 1)
blue_dots = 'bo'
solid_blue_line = 'b'
print('Lowest Generator Loss: ', np.argmin(g_loss))
print('Highest Discriminator Loss: ', np.argmax(d_loss))
plt.plot(epochs, d_loss, blue_dots, label = 'Discriminator Loss')
plt.plot(epochs, g_loss, solid_blue_line, label = 'Generator Loss')
plt.title('Discriminator and Generator loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
SHOW IMAGES
def show_imgs(modelz):
f,ax = plt.subplots(3,5,figsize=(15,10))
ax = ax.flatten()
arr = tf.random.normal(shape=(15, LATENT_DIM))
generated_portraits = modelz.generator(arr)
for i in range(15):
g=generated_portraits[i]*255
ax[i].imshow(tf.cast(g,tf.uint8))
SAVE / LOAD
#Save Paths
generator_path = basedir / "generator_flower_gan.h5"
discriminator_path = basedir / "discriminator_flower_gan.h5"
def save():
discriminator.save_weights(discriminator_path) # SAVE
generator.save_weights(generator_path)
def load():
discriminator_reloaded = create_discriminator((64, 64, 3)) # CREATE
generator_reloaded = create_generator(LATENT_DIM)
discriminator_reloaded.load_weights(discriminator_path) # LOAD WEIGHTS
generator_reloaded.load_weights(generator_path)
gan_reloaded = GAN( # REBUILD GAN
discriminator=discriminator_reloaded,
generator=generator_reloaded,
latent_dim=LATENT_DIM
)
return gan_reloaded
#Code provided in notebook 12.5
def make_model(optimrate):
discriminator_opt = tf.keras.optimizers.Adam(optimrate,0.5)
generator_opt = tf.keras.optimizers.Adam(optimrate,0.5)
loss_fn = tf.keras.losses.BinaryCrossentropy()
model = GAN(discriminator=discriminator, generator=generator, latent_dim=LATENT_DIM)
model.compile(d_optimizer=discriminator_opt, g_optimizer=generator_opt, loss_fn=loss_fn)
return model
Experiment 1:
Tweaking Optimiser Adam Optimizer Learning Rate, 1.5e-5.
Training on 40 epoch (initial) followed by further 50 epoch
model = make_model(1.5e-5)
history = train(model,40)
plot_loss()
show_imgs(model)
Retrain Further Epochs
discriminator_opt = tf.keras.optimizers.Adam(1.5e-5,0.5)
generator_opt = tf.keras.optimizers.Adam(1.5e-5,0.5)
loss_fn = tf.keras.losses.BinaryCrossentropy()
model = load()
model.compile(d_optimizer=discriminator_opt, g_optimizer=generator_opt, loss_fn=loss_fn)
history = train(model,50)
Model 1 Output
plot_loss()
show_imgs(model)
discriminator.save_weights(discriminator_path) # SAVE
generator.save_weights(generator_path)
Result Experiment 1:
Training on 40 epoch (initial)
Results: | Epoch | Discriminator Loss | Generator Loss | | --- | --- | --- | | 1 | 0.5583 | 0.9802 | | 2 | 0.3590 | 1.5348 | | 3 | 0.3804 | 1.6628 | | 4 | 0.1945 | 2.4467 | | 5 | 0.1654 | 3.1566 | | 6 | 0.2316 | 2.4135 | | 7 | 0.2805 | 2.0149 | | 8 | 0.3301 | 1.7659 | | 9 | 0.3881 | 1.6489 | | 10 | 0.4590 | 1.3642 | | 30 | 0.6279 | 0.9226 | | 31 | 0.6518 | 0.9712 | | 32 | 0.6944 | 0.8141 | | 33 | 0.7003 | 0.7937 | | 34 | 0.6823 | 0.8640 | | 35 | 0.6424 | 0.8935 | | 36 | 0.6399 | 0.8943 | | 37 | 0.6404 | 0.8319 | | 38 | 0.6531 | 0.8293 | | 39 | 0.6526 | 0.9012 | | 40 | 0.6621 | 0.8642 |
Further Training 50 Total Epoch: Results: | Epoch | Discriminator Loss | Generator Loss | | --- | --- | --- | | 1 | 0.6829 | 0.9802 | | 2 | 0.6807 | 0.7866 | | 3 | 0.6755 | 0.7899 | | 4 | 0.6755 | 0.7965 | | 5 | 0.6829 | 0.7960 | | 6 | 0.6729 | 0.7855 | | 7 | 0.6804 | 0.7822 | | 8 | 0.6790 | 0.8056 | | 9 | 0.6851 | 0.7840 | | 10 | 0.6774 | 0.7929 | | 40 | 0.6873 | 0.7605 | | 41 | 0.6903 | 0.7693 | | 42 | 0.6873 | 0.7714 | | 43 | 0.6900 | 0.7663 | | 44 | 0.6918 | 0.7644 | | 45 | 0.6867 | 0.7795 | | 46 | 0.6922 | 0.7680 | | 47 | 0.6844 | 0.7706 | | 48 | 0.6794 | 0.7805 | | 49 | 0.6874 | 0.7659 | | 50 | 0.6869 | 0.7696 |
The final model performance had a discriminator loss of 0.6869, which is a difference of 0.1286 from the first epoch of the initial run. On the other hand, the generator had a final value of 0.7696, with a difference of -0.2106. The discriminator loss remained relatively consistent between the final epochs, starting from the 30th to the 90th. The generator loss, however, gradually decreased across the range of epochs, indicating that further improvement is possible, albeit marginal. The final images produced were able to capture traits and patterns from the flower dataset, with many examples displaying realistic shapes and colors. A clear difference in detail can be observed between the initial image results and the final images.
Experiment 2:
Tweaking Optimiser Adam Optimizer Learning Rate, 2e-5.
Training on 40 epoch (initial) followed by further 50 epoch
model = make_model(2e-5)
history = train(model,40)
plot_loss()
show_imgs(model)
#Change Save Paths
generator_path = basedir / "generator_flower_gan_model2.h5"
discriminator_path = basedir / "discriminator_flower_gan_model2.h5"
save()
discriminator_opt = tf.keras.optimizers.Adam(2e-5,0.5)
generator_opt = tf.keras.optimizers.Adam(2e-5,0.5)
loss_fn = tf.keras.losses.BinaryCrossentropy()
model = load()
model.compile(d_optimizer=discriminator_opt, g_optimizer=generator_opt, loss_fn=loss_fn)
Model 2 Retrain + Results
history = train(model,50)
plot_loss()
show_imgs(model)
Result Experiment 2
Results: | Epoch | Discriminator Loss | Generator Loss | | --- | --- | --- | | 1 | 0.5402 | 1.0413 | | 2 | 0.3876 | 1.4123 | | 3 | 0.1702 | 2.4356 | | 4 | 0.1549 | 3.4074 | | 5 | 0.1523 | 2.8311 | | 6 | 0.1884 | 2.6452 | | 7 | 0.3844 | 2.5068 | | 8 | 0.4915 | 1.5771 | | 9 | 0.4941 | 1.2908 | | 10 | 0.5189 | 1.2442 | | 30 | 0.6476 | 0.8159 | | 31 | 0.6474 | 0.9005 | | 32 | 0.6549 | 0.8438 | | 33 | 0.6662 | 0.8448 | | 34 | 0.6718 | 0.8160 | | 35 | 0.6744 | 0.8023 | | 36 | 0.6832 | 0.7782 | | 37 | 0.6778 | 0.7963 | | 38 | 0.6732 | 0.8105 | | 39 | 0.6742 | 0.7923 | | 40 | 0.6885 | 0.8073 |
Further Training 50 Total Epoch:
Results: | Epoch | Discriminator Loss | Generator Loss | | --- | --- | --- | | 1 | 0.6829 | 0.8087 | | 2 | 0.6750 | 0.8094 | | 3 | 0.6883 | 0.7866 | | 4 | 0.6728 | 0.8005 | | 5 | 0.6716 | 0.8169 | | 6 | 0.6601 | 0.8255 | | 7 | 0.6730 | 0.8109 | | 8 | 0.6815 | 0.7924 | | 9 | 0.6710 | 0.7963 | | 10 | 0.6798 | 0.7979 | | 40 | 0.6814 | 0.7824 | | 41 | 0.6860 | 0.7826 | | 42 | 0.6820 | 0.7790 | | 43 | 0.6841 | 0.7813 | | 44 | 0.6764 | 0.7766 | | 45 | 0.6807 | 0.7845 | | 46 | 0.6758 | 0.7878 | | 47 | 0.6818 | 0.7972 | | 48 | 0.6794 | 0.7826 | | 49 | 0.6738 | 0.7846 | | 50 | 0.6764 | 0.7848 |
The results of the training show that the discriminator loss of model 2 decreased from 0.5402 in the first epoch to 0.6764 in the final epoch, with a difference of 0.1362. The generator loss, on the other hand, had a larger difference compared to model 1, with a difference of -0.2565. The final results of the initial training (up to 40 epochs) were similar to the final training epoch, with only a 0.02 difference. This suggests that the model is able to achieve similar statistical results without the need for further training, resulting in a reduction in computational expense. However, there is a notable difference in the quality of the images produced by the further trained model, with a clearer image, more vibrancy, and more detail. The next experiment will involve increasing the model's capacity in an attempt to decrease and increase the loss of the GAN model at the current learning rate.
save()
Experiment 3:
Increase model capacity by doubling units of every Conv Layer from initial model by 2x. The aim is that by increasing capacity, the model will be able to extract further basic texture features within the lower layers and provide a increased definition on the produced images.
#Redefine the generator and discriminator
def create_generator(latent_dim):
generator=Sequential()
generator.add(Dense(4*4*512,input_shape=[latent_dim]))
generator.add(Reshape([4,4,512]))
generator.add(Conv2DTranspose(256, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(512, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(1024, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(3, kernel_size=4, strides=2, padding="same", activation='sigmoid'))
return generator
generator = create_generator(LATENT_DIM)
generator.summary()
def create_discriminator(input_shape):
discriminator=Sequential()
discriminator.add(Conv2D(128, kernel_size=4, strides=2, padding="same",input_shape=input_shape))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(256, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(512, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Flatten())
discriminator.add(Dropout(0.2))
discriminator.add(Dense(1,activation='sigmoid'))
return discriminator
input_shape = (64, 64, 3)
discriminator = create_discriminator(input_shape)
discriminator.summary()
model = make_model(2e-5)
history = train(model,40)
plot_loss()
show_imgs(model)
#Change Save Paths
generator_path = basedir / "generator_flower_gan_model3.h5"
discriminator_path = basedir / "discriminator_flower_gan_model3.h5"
save()
history = train(model,50)
Model 3 Outputs:
plot_loss()
show_imgs(model)
save()
Result Experiment 3
The results of this experiment indicate that the loss values of the generator and discriminator did not reach the same levels as in experiment 2, with a difference of (-0.0297/+0.0935) respectively. Despite this, the final image outputs have a much clearer definition compared to experiment 2. This is likely due to the higher capacity of the model allowing for the discovery of more representative features. However, this increase in capacity also seems to have caused a generic pattern. As such, the next experiment will involve increasing the model's units as per experiment 2, but at a lower factor than in experiment 3.
Experiment 4
Increase model capacity of model 2 by increasing Conv2D Layers by 1.5x,
#Change discriminator and generator units factor of 1.5 from original
def create_generator(latent_dim):
generator=Sequential()
generator.add(Dense(4*4*512,input_shape=[latent_dim]))
generator.add(Reshape([4,4,512]))
generator.add(Conv2DTranspose(192, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(384, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(758, kernel_size=4, strides=2, padding="same"))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(3, kernel_size=4, strides=2, padding="same", activation='sigmoid'))
return generator
generator = create_generator(LATENT_DIM)
generator.summary()
def create_discriminator(input_shape):
discriminator=Sequential()
discriminator.add(Conv2D(96, kernel_size=4, strides=2, padding="same",input_shape=input_shape))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(192, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(384, kernel_size=4, strides=2, padding="same"))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Flatten())
discriminator.add(Dropout(0.2))
discriminator.add(Dense(1,activation='sigmoid'))
return discriminator
input_shape = (64, 64, 3)
discriminator = create_discriminator(input_shape)
discriminator.summary()
model = make_model(2e-5)
history = train(model,40)
plot_loss()
show_imgs(model)
#Change Save Paths
generator_path = basedir / "generator_flower_gan_model4.h5"
discriminator_path = basedir / "discriminator_flower_gan_model4.h5"
save()
#Further 50 Epochs
history = train(model,50)
plot_loss()
show_imgs(model)
save()
Results Experiment 4
Parameter Change: | Algorithm | Experiment 3 Trainable Parameters | Experiment 4 Trainable Parameters | Difference | | --- | --- | --- | --- | | Generator | 13,697,795 | 8,509,489 | 5,188,306 | | Discriminator | 2,662,785 | 1,505,569 | 1,157216 |
3:0.6467 - g_loss: 0.8783
4:0.6669 - g_loss: 0.8196
The results of the study on the generator and discriminator indicate an improvement in performance, as evidenced by the change in loss of +0.0202/-0.0587 compared to experiment 3. Despite this improvement, the model still lacks definition and produces images with a higher level of vibrancy than the previous model. The experiment as a whole suggests that increasing the capacity of the model is necessary for achieving better image output, which is the primary focus of this investigation. This is supported by the fact that the loss values in experiment 1 were lower at (0.6669 / 0.8196) than in the current experiment.
Experiment 5
Alter model 3 to add L2 regularization, reload weights and train further
#Add L2 Regularization
from keras import regularizers
def create_generator(latent_dim):
generator=Sequential()
generator.add(Dense(4*4*512, input_shape=[latent_dim], kernel_regularizer=regularizers.l2(0.001)))
generator.add(Reshape([4,4,512]))
generator.add(Conv2DTranspose(256, kernel_size=4, strides=2, padding="same", kernel_regularizer=regularizers.l2(0.001)))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(512, kernel_size=4, strides=2, padding="same", kernel_regularizer=regularizers.l2(0.001)))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(1024, kernel_size=4, strides=2, padding="same", kernel_regularizer=regularizers.l2(0.001)))
generator.add(LeakyReLU(alpha=0.2))
generator.add(BatchNormalization())
generator.add(Conv2DTranspose(3, kernel_size=4, strides=2, padding="same", activation='sigmoid', kernel_regularizer=regularizers.l2(0.001)))
return generator
generator = create_generator(LATENT_DIM)
generator.summary()
def create_discriminator(input_shape):
discriminator=Sequential()
discriminator.add(Conv2D(128, kernel_size=4, strides=2, padding="same",input_shape=input_shape, kernel_regularizer=regularizers.l2(0.001)))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(256, kernel_size=4, strides=2, padding="same", kernel_regularizer=regularizers.l2(0.001)))
discriminator.add(LeakyReLU(0.2))
discriminator.add(BatchNormalization())
discriminator.add(Conv2D(512, kernel_size=4, strides=2, padding="same", kernel_regularizer=regularizers.l2(0.001)))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Flatten())
discriminator.add(Dropout(0.2))
discriminator.add(Dense(1,activation='sigmoid', kernel_regularizer=regularizers.l2(0.001)))
return discriminator
input_shape = (64, 64, 3)
discriminator = create_discriminator(input_shape)
discriminator.summary()
#Change Load Paths To Model 3
generator_path = basedir / "generator_flower_gan_model3.h5"
discriminator_path = basedir / "discriminator_flower_gan_model3.h5"
load()
discriminator_opt = tf.keras.optimizers.Adam(2e-5,0.5)
generator_opt = tf.keras.optimizers.Adam(2e-5,0.5)
loss_fn = tf.keras.losses.BinaryCrossentropy()
model = load()
model.compile(d_optimizer=discriminator_opt, g_optimizer=generator_opt, loss_fn=loss_fn)
history = train(model,200)
plot_loss()
show_imgs(model)
save()
Results Experiment 5
After training model 3 on a further 200 epochs, it can bee seen that the lowest generator loss was found at epoch 113 (index change) with a value of 0.8189, still higher than experiment 1/2. The outputs generated by experiment 5 provide an interesting look at the features being identified by the lower layers of the model through the generation of some rather trippy looking alien like flowers.
Conclusion
Throughout the course of this investigation, the objective of producing realistic flower art using GANs has been explored through the tuning of various hyperparameters, such as layer units and learning rate, as well as the application of various regularization techniques. The impact of these changes on the performance of the GAN model was evaluated through the analysis of the loss values of both the generator and discriminator, as well as the examination of the final generated images.
It can be concluded that a comprehensive design process for GAN models has been successfully demonstrated, as evidenced by the high quality of the generated flower images and the insights gained from the experiments conducted. However, it is important to note that further experimentation is necessary to optimize the model's performance. Specifically, it is recommended to employ smaller models and to fine-tune the model's hyperparameters in order to reduce the overall loss, while also scaling up the model's capacity when appropriate.